import { ActionTypes } from './createStore'
import isPlainObject from 'lodash/isPlainObject'
import warning from './utils/warning'

function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionName = (actionType && `"${actionType.toString()}"`) || 'an action'

  return (
    `Given action ${actionName}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
  const reducerKeys = Object.keys(reducers)
  const argumentName = action && action.type === ActionTypes.INIT ?
    'preloadedState argument passed to createStore' :
    'previous state received by the reducer'

  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  const unexpectedKeys = Object.keys(inputState).filter(key =>
    !reducers.hasOwnProperty(key) &&
    !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}

function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined. If you don't want to set a value for this reducer, ` +
        `you can use null instead of undefined.`
      )
    }

    const type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

/**
 *
 * 将值为不同reducer函数的对象转换为单个reducer函数。 它将调用每个子reducer，并将其结果收
 * 集到一个状态对象中，该对象的键对应于传递的reducer函数的键。
 *
 * @param {Object} 一个对象，它的值（value）对应不同的 reducer 函数，这些 reducer 函数
 * 后面会被合并成一个。下面会介绍传入 reducer 函数需要满足的规则。
 *
 * (Function)：一个调用 reducers 对象里所有 reducer 的 reducer，
 * 并且构造一个与 reducers 对象结构相同的 state 对象。
 */
export default function combineReducers(reducers) {
  //返回reducers对象里边的所以key值
  const reducerKeys = Object.keys(reducers)
  //过滤后的reducers存放
  const finalReducers = {}
  /**
   * 遍历 reducers 对象上的属性，
   * 1. 做非空校验
   * 2. 保留属性为 function 类型的 reducer
   */
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      //存放满足条件的key值
      finalReducers[key] = reducers[key]
    }
  }

  //获取过滤finalReducers的key值
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  //定义完善性检测异常
  let shapeAssertionError
  try {
    /**
     * 完善性检测
     * 检验 finalReducers 对象上的每一个子 reducer 能否按照 redux 定义的规则处理 action，
     * 并 return 正确的 state。
     * 如果不满足完善性检测，则抛出异常
     */
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // combineReducers 函数返回一个函数，也就是合并后的 reducer 函数
  // 该函数返回总的 state
  // 并且你也可以发现这里使用了闭包，函数里面使用到了外面的一些属性
  return function combination(state = {}, action) {
    // 如果未通过完善性检测，则中断并抛出异常。
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    //标志state是否发现变化
    let hasChanged = false
    // 定义该对象用于保存更改后的 state 树
    const nextState = {}
    //再一次遍历 finalReducerKeys 对象上的子 reducer 属性。
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      /**
       * 获取变更前 state 上与 reducer key 相对应的部分 state。
       * 如果是初始化阶段，该 state 分支的值是 undefined。
       * 如果非初始化阶段，则是获取 state 分支的值。
       */
      var previousStateForKey = state[key]
      // 用当前 reducer 计算 action，返回计算后的部分 state。
      var nextStateForKey = reducer(previousStateForKey, action)
      // 检测一下当前 reducer 有没有正确返回 state。
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 将子 reducer 计算后的部分 state 挂在对应的 state 树属性上
      nextState[key] = nextStateForKey
      /**
       * 这里是对比每个 nextStateForKey 与 previousStateForKey，通过短路写法可以提高效率。
       * 对象相比比地址，如果 nextStateForKey 是 reducer 返回的新的 state，
       * 与 previousStateForKey 地址不同 hasChanged 就会变 true。
       *
       * reducer 的规则是，若要在 reducer 里面更新 state，不是直接修改 state，
       * 而是开辟新的地址修改并存放 newState，然后最终作为 reducer 的返回值。
       * 根据该规则，如果 state “变更” 了，其 nextStateForKey 和 previousStateForKey 地址
       * 不相等。
       */
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果未改变，则返回旧 state，相应地 UI 方面也不用回流。
    return hasChanged ? nextState : state
  }
}
